package lang.asm;

import editor.ErrorHandler;
import lang.Language;
import lang.Loader;
import model.code.line.ASMLine;
import static model.code.line.MemoryOperand.*;
import model.code.line.MultiLineList;
import model.symbol.*;
import util.FileUtil;
import util.MemoryUtil;
import util.StringUtil;
import xml.Element;
import xml.Tag;
import xml.XML;

import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.HashMap;

/**
 * This class wraps the core assembly language functionality.
 *
 * NOTE: Although I wouldn't normally be in favour of this type of tree of singletons, the large volume of code requires some sort of organisational structure like this.
 *
 * @author Neil Dickson
 */
public class ASM extends Language {
	public static final ASM language = new ASM();
	public static final String		NAME						= "assembly";
	public static final String		SOURCE_EXTENSION			= "asm";
	public static final String		INCLUDE_EXTENSION			= "inc";
	/**
	 * These are the special characters that can be the first letter of the first symbol on a line.
	 */
	public static final String		FIRST_WORD_SPECIAL_CHARS	= "._$?@";
	/**
	 * These are the special characters that can be part of any symbol on a line.
	 */
	public static final String		WORD_SPECIAL_CHARS			= "_$?@";

	/**
	 * Content of a comment marking an enumeration value generated for compatibility, but not specified in the editor
	 */
	public static final String		ENUMERATION_VALUE_MARKER	= "*EnumerationValue*";
	public static final Keyword		ASSUME						= new Keyword(true,"assume","Keyword used to make and clear register type assumptions.\nexample:\nASSUME EAX:PTR MY_STRUCT                    ;adds assumption that eax holds address of structure\n    mov     edx,[eax].memberOfMyStructure   ;accesses member of structure MY_STRUCT");
	public static final Keyword		NOTHING						= new Keyword("nothing","Indicates no type. used with \"assume\" to remove assumptions of a register's type");
	public static final Keyword		PTR							= new Keyword("ptr","Short for \"pointer\".\nexamples:\nmov al,byte ptr myDword ;moves low byte of myDword into al.\nASSUME EAX:PTR MY_STRUCT ;adds assumption that eax holds address of structure");

	public static final Keyword		CODE_SEGMENT				= new Keyword(".code","The following file content is in the default code segment.");
	public static final Keyword		INIT_DATA_SEGMENT			= new Keyword(".data","The following file content is in the default initialised data segment.");
	public static final Keyword		UNINIT_DATA_SEGMENT			= new Keyword(".data?","The following file content is in the default uninitialised data segment.");
	public static final Keyword		READ_ONLY_DATA_SEGMENT		= new Keyword(".const","The following file content is in the default read-only data segment.");

	public static final Keyword			DUP					= new Keyword("dup","Used for repeating (duplicating) a value in the initial value of a global variable or structure member\nexamples:\n256 DUP (?) ;256 undefined elements\n256 DUP (0) ;256 elements of value 0\n3 DUP (\"a\",\"b\") ;\"a\",\"b\"\"a\",\"b\"\"a\",\"b\"\n4 dup (<>)  ;4 structures with their default initial values\nDEBUG dup (<0,\"F\">,<1,\"T\">)  ;2 structures if DEBUG=TRUE and nothing if DEBUG=FALSE\n2 dup (123,2 dup (456))  ;123,456,456,123,456,456");
	public static final Keyword			UNDEFINED_VALUE		= new Keyword("?","Marks an initial value as undefined (i.e. may not have a known value until written-over)\nThis is frequently used in global variables and structure members.");
	public static final LineLabel		HERE				= new LineLabel(new char[]{'$'},"The address of the beginning of the line of code on which it appears.\nIt is most often used for \"jmp $\" to loop infinitely (like \"while (TRUE) {}\" in C).".toCharArray(),null);
	public static final Keyword			AT_F				= new Keyword("@f","Reference to the next (forward) \"@@:\" line label.\nexample:\nmov al,[esi]    ;moves byte pointed to by esi into al\ncmp al,\" \"      ;compares al to the space character\nje  @F          ;skips the next instruction if there's a space character in al\ninc esi         ;moves esi past the space character\n@@:             ;the target line label of the conditional jump instruction");
	public static final Keyword			AT_B				= new Keyword("@b","Reference to the previous (backward) \"@@:\" line label.\nexample:\n;The following bit of code will loop until another thread clears bit 0 of LockVariable if another thread has set the bit, and otherwise proceed\n@@:                             ;the target line label of the conditional jump instruction\n    lock bts    LockVariable,0  ;sets bit 0 of LockVariable and clears/sets the carry flag based on the previous value of the bit\n    jc          @B              ;loops backward if the bit was set before the previous instruction");
	public static final Keyword			AT_AT				= new Keyword("@@","Anonymous line label for use with @F and @B.\nexample:\n;The following bit of code will loop until another thread clears bit 0 of LockVariable if another thread has set the bit, and otherwise proceed\n@@:                             ;the target line label of the conditional jump instruction\n    lock bts    LockVariable,0  ;sets bit 0 of LockVariable and clears/sets the carry flag based on the previous value of the bit\n    jc          @B              ;loops backward if the bit was set before the previous instruction");
	public static final Keyword			IF					= new Keyword(true,"if","Compile-time conditional assembly: assemble following statements if supplied condition is true (non-zero).\nexample:\nIF DEBUGGING\n    invoke  OutputDebugData ;only appears in the compiled program if DEBUGGING is defined as true\nENDIF");
	public static final Keyword			ELSE				= new Keyword(true,"else","Compile-time conditional assembly: assemble following statements if condition in associated \"if\" is false (zero).\nexample:\nIF DEBUGGING\n    invoke  OutputDebugData ;only appears in the compiled program if DEBUGGING is defined as true\nELSE\n    invoke  OutputData      ;only appears if DEBUGGING is false\nENDIF");
	public static final Keyword			ELSEIF				= new Keyword(true,"elseif","Compile-time conditional assembly: assemble following statements if condition in associated \"if\" is false (zero) and this condition is true (non-zero).\nexample:\nIF DEBUGGING\n    invoke  OutputDebugData         ;only appears in the compiled program if DEBUGGING is defined as true\nELSEIF PERFORMANCE_TESTING\n    invoke  OutputPerformanceData   ;only appears if DEBUGGING is false and PERFORMANCE_TESTING is true\nELSE\n    invoke  OutputData              ;only appears if DEBUGGING and PERFORMANCE_TESTING are false\nENDIF");
	public static final Keyword			ENDIF				= new Keyword(true,"endif","End of compile-time conditional assembly (see \"if\").\nexample:\nIF DEBUGGING\n    invoke  OutputDebugData ;only appears in the compiled program if DEBUGGING is defined as true\nENDIF");
	public static final Keyword			ERR					= new Keyword(true,".err","Forces a compile-time error.\nexample:\n;The following line will generate a compile error with the specified message\n.err <Don't try running without implementing this part first!>");
	public static final Keyword			INVOKE				= new Keyword(true,"invoke","Calls a function using the function's calling convention.\nexample:\n;The following line will generate push 3, push MyVariable, push eax, call MyFunction\ninvoke MyFunction,eax,MyVariable,3");
	public static final Keyword			REGISTER			= new KeywordDataType("register","Indicates that a parameter is the register specified in the name field instead of being space on the stack.\nexample:\neax     register    address of the array of points",0);
	public static final Keyword			REQ					= new KeywordDataType("req","Indicates that a parameter is a required text-replacement parameter for an inline function.\nAny references to a parameter with this type will be replaced with text specified in the function call.",0);
	public static final KeywordDataType DB					= new KeywordDataType("db","8 bits",1);
	public static final KeywordDataType	DW					= new KeywordDataType("dw","2 bytes, 16 bits",2);
	public static final KeywordDataType	DD					= new KeywordDataType("dd","4 bytes, 32 bits",4);
	public static final KeywordDataType	DF					= new KeywordDataType("df","6 bytes, 48 bits",6);
	public static final KeywordDataType	DQ					= new KeywordDataType("dq","8 bytes, 64 bits",8);
	public static final KeywordDataType	DT					= new KeywordDataType("dt","10 bytes, 80 bits",10);
	public static final KeywordDataType SBYTE				= new KeywordDataType("sbyte","8 bits",1);
	public static final KeywordDataType	SWORD				= new KeywordDataType("sword","2 bytes, 16 bits",2);
	public static final KeywordDataType	SDWORD				= new KeywordDataType("sdword","4 bytes, 32 bits",4);
	public static final KeywordDataType BYTE				= new KeywordDataType("byte","8 bits",1);
	public static final KeywordDataType	WORD				= new KeywordDataType("word","2 bytes, 16 bits",2);
	public static final KeywordDataType	DWORD				= new KeywordDataType("dword","4 bytes, 32 bits",4);
	public static final KeywordDataType	FWORD				= new KeywordDataType("fword","6 bytes, 48 bits",6);
	public static final KeywordDataType	QWORD				= new KeywordDataType("qword","8 bytes, 64 bits",8);
	public static final KeywordDataType	TBYTE				= new KeywordDataType("tbyte","10 bytes, 80 bits",10);
	public static final KeywordDataType	XMMWORD				= new KeywordDataType("xmmword","16 bytes, 128 bits",16);
	public static final KeywordDataType	REAL4				= new KeywordDataType("real4","4-byte floating-point number, 32 bits",4);
	public static final KeywordDataType	REAL8				= new KeywordDataType("real8","8-byte floating-point number, 64 bits",8);
	public static final KeywordDataType	REAL10				= new KeywordDataType("real10","10-byte floating-point number, 80 bits",10);

	public final ArrayList<String>		instructionGroups;
	public final ArrayList<Mnemonic>	mnemonics;
	public final ArrayList<Register>	registers;

	public final byte[][]				addressingTypes16;
	public final short[][]				addressingEncodings16;
	public final byte[][]				addressingTypes32;
	public final short[][]				addressingEncodings32;
	public final byte[][]				addressingTypes64;
	public final short[][]				addressingEncodings64;

	private ASM() {
		instructionGroups = new ArrayList<String>();
		mnemonics = new ArrayList<Mnemonic>();
		registers = new ArrayList<Register>();
		byte[][] addressingTypes16 = null;
		short[][] addressingEncodings16 = null;
		byte[][] addressingTypes32 = null;
		short[][] addressingEncodings32 = null;
		byte[][] addressingTypes64 = null;
		short[][] addressingEncodings64 = null;
		try {
			MemoryUtil.gc();
			MemoryUtil.logMemory();
			XML xml = new XML(FileUtil.readFileString("ASM.xml"),true);
			MemoryUtil.gc();
			MemoryUtil.logMemory();
			String[] memoryTypeMap = {"r","r*2","r*4","r*8","rbp","rbp*2","rbp*4","rbp*8","ebp","ebp*2","ebp*4","ebp*8","bp","rsp","esp","rip","bx","si","di","r12","imm8s","imm16s","imm32s","imm16u","imm32u","imm16","imm32"};
			byte[] memoryTypeMapped = {MATCH_R,MATCH_R_2,MATCH_R_4,MATCH_R_8,MATCH_RBP,MATCH_RBP_2,MATCH_RBP_4,MATCH_RBP_8,MATCH_RBP,MATCH_RBP_2,MATCH_RBP_4,MATCH_RBP_8,MATCH_RBP,MATCH_RSP,MATCH_RSP,MATCH_RIP,MATCH_BX,MATCH_SI,MATCH_DI,MATCH_R12,MATCH_IMM8S,MATCH_IMM16S,MATCH_IMM32S,MATCH_IMM16U,MATCH_IMM32U,MATCH_IMM16,MATCH_IMM32};
			Tag root = xml.getRoot();
			ArrayList<Element> children = root.getChildren();
			for (Element child : children) {
				if (child instanceof Tag) {
					Tag tag = (Tag)child;
					if (tag.getName().equals("registers")) {
						ArrayList<Element> registerTags = tag.getChildren();
						for (Element element : registerTags) {
							if (element instanceof Tag) {
								Tag registerTag = (Tag)element;
								String[] names = StringUtil.fromCommaSeparatedList(registerTag.getAttribute("name"));
								String description = registerTag.getAttribute("desc");
								description = (description!=null) ? description : "";
								int type = DataTypeUtil.getOperandTypeFromName(registerTag.getAttribute("type"));
								int number = registerTag.hasAttribute("num") ? Integer.parseInt(registerTag.getAttribute("num")) : -1;
								for (String name : names) {
									Register register = new Register(name,description,type,number);
									scope.add(register);
									registers.add(register);
								}
							}
						}
					}
					else if (tag.getName().equals("instructions")) {
						ArrayList<Element> instructionTags = tag.getChildren();
						for (Element element : instructionTags) {
							if (element instanceof Tag) {
								Tag instructionTag = (Tag)element;
								String name = instructionTag.getAttribute("name");
								String description = instructionTag.getAttribute("desc");
								String[] groups = StringUtil.fromCommaSeparatedList(instructionTag.getAttribute("groups"));
								Mnemonic mnemonic = null;
								if (scope.contains(name)) {
									ArrayList<Symbol> symbols = scope.get(name);
									for (Symbol symbol : symbols) {
										if (symbol instanceof Mnemonic) {
											mnemonic = (Mnemonic)symbol;
											break;
										}
									}
								}
								if (mnemonic==null) {
									mnemonic = new Mnemonic(name,description);
									mnemonics.add(mnemonic);
								}
								ArrayList<Element> operandTags = instructionTag.getChildren();
								for (Element opElement : operandTags) {
									if (opElement instanceof Tag) {
										Tag operandTag = (Tag)opElement;
										String[] opStrings = StringUtil.fromCommaSeparatedList(operandTag.getAttribute("operands"));
										if (opStrings.length==1 && opStrings[0].length()==0) {
											opStrings = new String[0];
										}
										Object[] ops = new Object[opStrings.length];
										for (int i=0;i<opStrings.length;++i) {
											int type = DataTypeUtil.getOperandTypeFromName(opStrings[i]);
											if (type!=DataTypeUtil.TYPE_INDEX_UNKNOWN) {
												ops[i] = type;
											}
											else {
												ops[i] = opStrings[i];
											}
										}
										short[] encoding16;
										short[] encoding32;
										short[] encoding64;
										if (operandTag.hasAttribute("encoding")) {
											encoding16 = encodingFromString(operandTag.getAttribute("encoding"));
											encoding32 = encoding16;
											encoding64 = encoding16;
										}
										else {
											encoding16 = operandTag.hasAttribute("encoding16") ? encodingFromString(operandTag.getAttribute("encoding16")) : null;
											encoding32 = operandTag.hasAttribute("encoding32") ? encodingFromString(operandTag.getAttribute("encoding32")) : null;
											encoding64 = operandTag.hasAttribute("encoding64") ? encodingFromString(operandTag.getAttribute("encoding64")) : null;
										}
										String newDescription = description;
										if (operandTag.hasAttribute("desc")) {
											newDescription = operandTag.getAttribute("desc");
										}
										mnemonic.addOperands(ops,encoding16,encoding32,encoding64,groups,newDescription);
									}
								}
								scope.add(mnemonic);
							}
						}
					}
					else if (tag.getName().equals("instructionGroups")) {
						ArrayList<Element> groupTags = tag.getChildren();
						for (Element element : groupTags) {
							if (element instanceof Tag) {
								Tag groupTag = (Tag)element;
								instructionGroups.add(groupTag.getAttribute("name"));
							}
						}
					}
					else if (tag.getName().equals("memory")) {
						ArrayList<Element> memoryTags = tag.getChildren();
						int mode = Integer.parseInt(tag.getAttribute("mode"));
						int count = Integer.parseInt(tag.getAttribute("count"));
						byte[][] types = new byte[count][];
						short[][] encodings = new short[count][];
						int index = 0;
						for (Element element : memoryTags) {
							if (element instanceof Tag) {
								Tag memoryTag = (Tag)element;
								String[] typeStrings = memoryTag.getAttribute("type").split("\\+");
								String[] encodingStrings = memoryTag.getAttribute("encoding").split(" ");
								byte[] type = new byte[typeStrings.length];
								short[] encoding = new short[encodingStrings.length];
								for (int i=0;i<type.length;++i) {
									for (int j=0;j<memoryTypeMap.length;++j) {
										if (typeStrings[i].equals(memoryTypeMap[j])) {
											type[i] = memoryTypeMapped[j];
											break;
										}
									}
								}
								for (int i=0;i<encoding.length;++i) {
									if (encodingStrings[i].equals("im8")) {
										encoding[i] = ASMEncode.M_IMM8;
									}
									else if (encodingStrings[i].equals("im16")) {
										encoding[i] = ASMEncode.M_IMM16;
									}
									else if (encodingStrings[i].equals("im32")) {
										encoding[i] = ASMEncode.M_IMM32;
									}
									else if (encodingStrings[i].endsWith("+RA")) {
										encoding[i] = (short)(ASMEncode.M_PLUS_RA_START+Short.parseShort(encodingStrings[i].substring(0,2),16));
									}
									else if (encodingStrings[i].endsWith("+RA*8")) {
										encoding[i] = (short)(ASMEncode.M_PLUS_RA8_START+Short.parseShort(encodingStrings[i].substring(0,2),16));
									}
									else if (encodingStrings[i].endsWith("+RA+RB*8")) {
										encoding[i] = (short)(ASMEncode.M_PLUS_RA_RB8_START+Short.parseShort(encodingStrings[i].substring(0,2),16));
									}
									else {
										encoding[i] = Short.parseShort(encodingStrings[i],16);
									}
								}
								types[index] = type;
								encodings[index] = encoding;
								++index;
							}
						}
						if (mode==16) {
							addressingTypes16 = types;
							addressingEncodings16 = encodings;
						}
						else if (mode==32) {
							addressingTypes32 = types;
							addressingEncodings32 = encodings;
						}
						else { // mode==64
							addressingTypes64 = types;
							addressingEncodings64 = encodings;
						}
					}
				}
			}
			Loader.EXTENSION_TO_LANGUAGE_MAP.put(SOURCE_EXTENSION,this);
			Loader.EXTENSION_TO_LANGUAGE_MAP.put(INCLUDE_EXTENSION,this);
			Loader.NAME_TO_LANGUAGE_MAP.put(NAME,this);
			MemoryUtil.logMemory();
			xml = null;
			MemoryUtil.gc();
		}
		catch (FileNotFoundException e) {
			scope = null;
			ErrorHandler.showMessage("The assembly language data file ASM.xml is missing,\nso assembly language won't work.  Sorry.");
		}
		catch (Throwable t) {
			scope = null;
			ErrorHandler.showException(t,"An error occured while loading the assembly language data file ASM.xml");
		}
		this.addressingTypes16 = addressingTypes16;
		this.addressingEncodings16 = addressingEncodings16;
		this.addressingTypes32 = addressingTypes32;
		this.addressingEncodings32 = addressingEncodings32;
		this.addressingTypes64 = addressingTypes64;
		this.addressingEncodings64 = addressingEncodings64;
	}
	private short[] encodingFromString(String s) {
		String[] tokens = s.split(" ");
		short[] result = new short[tokens.length];
		for (int i=0;i<tokens.length;++i) {
			String token = tokens[i];
			if (token.length()==2) {
				if (StringUtil.isHex(token.charAt(0)) && StringUtil.isHex(token.charAt(1))) {
					result[i] = (short)(StringUtil.fromHex(token.charAt(1)) | (StringUtil.fromHex(token.charAt(0))<<4));
				}
				else if (token.charAt(0)=='m' && token.charAt(1)>='0' && token.charAt(1)<='7') {
					result[i] = (short)(ASMEncode.MEM_NUM_START+StringUtil.fromHex(token.charAt(1)));
				}
				else if (token.equals("mr")) {
					result[i] = ASMEncode.MEM_REG;
				}
				else if (token.equals("rm")) {
					result[i] = ASMEncode.REG_MEM;
				}
				else if (token.equals("im")) {
					result[i] = ASMEncode.IMM;
				}
				else if (token.equals("rr")) {
					result[i] = ASMEncode.DOUBLE_REG;
				}
				else if (token.equals("rx")) {
					result[i] = ASMEncode.REX;
				}
				else if (token.equals("js")) {
					result[i] = ASMEncode.JMP_SHORT;
				}
				else if (token.equals("jn")) {
					result[i] = ASMEncode.JMP_NEAR;
				}
				else {
					System.err.println("Oh noes!  The token \""+token+"\" isn't handled yet!");
				}
			}
			else if (token.length()==3) {
				if (token.equals("rxw")) {
					result[i] = ASMEncode.REX_W;
				}
				else if (token.equals("rxo")) {
					result[i] = ASMEncode.REX_OPTIONAL;
				}
				else if (token.equals("jcs")) {
					result[i] = ASMEncode.JCC_SHORT;
				}
				else if (token.equals("jso")) {
					result[i] = ASMEncode.JMP_SHORT_ONLY;
				}
				else {
					System.err.println("Oh noes!  The token \""+token+"\" isn't handled yet!");
				}
			}
			else if (token.length()==4 && StringUtil.isHex(token.charAt(0)) && StringUtil.isHex(token.charAt(1)) && token.charAt(2)=='+' && token.charAt(3)=='R') {
				result[i] = (short)(ASMEncode.PLUS_R_START+(StringUtil.fromHex(token.charAt(1)) | (StringUtil.fromHex(token.charAt(0))<<4)));
			}
			else {
				System.err.println("Oh noes!  The token \""+token+"\" isn't handled yet!");
			}
		}
		return result;
	}
	public ASMLoad getLoader() {
		return ASMLoad.instance;
	}
	public ASMSave getSaver() {
		return ASMSave.instance;
	}
	public ASMDoc getDocParser() {
		return ASMDoc.instance;
	}
	public ASMParse getParser() {
		return ASMParse.instance;
	}
	public ASMError getErrorDetector() {
		return ASMError.instance;
	}
	public ASMEncode getEncoder() {
		return ASMEncode.instance;
	}
	public String getName() {
		return NAME;
	}

	public String getDefaultExtension() {
		return SOURCE_EXTENSION;
	}

	public String getDefaultIncludeExtension() {
		return INCLUDE_EXTENSION;
	}

	public ASMLine createCodeLine(MultiLineList list, int index, char[] text) {
		HashMap<Register,DataType> assumptions;
		if (index<=list.size() && index>0) {
			assumptions = ((ASMLine)list.get(index-1)).getAssumptions();
		}
		else if (index==0 && list.size()>0 && ((ASMLine)list.get(0)).getAssumptions().isEmpty()) {
			assumptions = ((ASMLine)list.get(0)).getAssumptions();
		}
		else {
			assumptions = new HashMap<Register, DataType>(1);
		}
		return new ASMLine(list,text,assumptions);
	}
}
